/**
 * Copyright (C) 2010 David Simonek <dafesimonek@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package org.dafe.partyband.importer.impl;

import com.echonest.api.v4.Artist;
import com.echonest.api.v4.EchoNestAPI;
import com.echonest.api.v4.EchoNestException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.dafe.partyband.db.api.IAuthor;
import org.dafe.partyband.db.api.IBandManager;
import org.dafe.partyband.db.api.IBandMember;
import org.dafe.partyband.db.api.IBandModelFactory;
import org.dafe.partyband.db.api.ISong;
import org.dafe.partyband.db.api.ISongResource;
import org.dafe.partyband.db.api.handlers.ISongHandler;
import org.dafe.partyband.db.api.queries.IAuthorQuery;
import org.dafe.partyband.db.api.queries.ISongQuery;
import org.dafe.partyband.importer.api.IDirectoryScanner;
import org.dafe.partyband.importer.api.IImportObserver;
import org.dafe.partyband.importer.api.IImportRecipe;
import org.dafe.partyband.importer.api.IMimeTypeDetector;
import org.dafe.partyband.importer.api.INameEncoder;
import org.dafe.partyband.importer.api.IScanResult;
import org.dafe.partyband.importer.api.ISongImport;
import org.dafe.partyband.importer.api.ISongImport.IEvaluation;
import org.dafe.partyband.importer.api.ISongImport.IImportIssue;
import org.dafe.partyband.importer.api.ISongImporter;
import org.dafe.partyband.importer.api.ITitleCorrector;
import org.dafe.partyband.importer.api.Utils;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;

/**
 *
 * @author dafe
 */
public class SongImporter implements ISongImporter, ISongImport.IEvaluator {

    private IDirectoryScanner dirScanner;
    private IMimeTypeDetector mimeDetector;
    private INameEncoder encoder;
    private IBandModelFactory dbFactory;
    private IBandManager dbManager;
    private EchoNestAPI echoNest;
    private ITitleCorrector titleCorrector;

    private List<IImportObserver> ioList = new ArrayList<IImportObserver>(1);

    public IImportRecipe prepare() {
        assert dirScanner != null : "Need directory scanner";
        assert mimeDetector != null : "Need mime detector";
        assert encoder != null : "Need name encoder";
        assert dbFactory != null : "Need Db model factory";
        assert dbManager != null : "Need Db manager";

        IScanResult scanResult = dirScanner.scan();
        IImportRecipe result = new ImportRecipe();
        ISongQuery songQuery = dbManager.getQueriesLookup().lookup(ISongQuery.class);

        List<? extends ISong> allSongsInDb = songQuery.getAllSongs();

        for (String rootName : scanResult.getRootNames()) {
            NameAuthorInfo nameAuthorInfo = guessSongNameAndAuthor(rootName);
            ISong songInDb = nameAuthorInfo.author != null ? 
                findSong(nameAuthorInfo.name, allSongsInDb) : null;

            if (titleCorrector != null) {
                if (nameAuthorInfo.name != null) {
                    nameAuthorInfo.name = titleCorrector.getSongName(nameAuthorInfo.name);
                }
                if (nameAuthorInfo.author != null) {
                    nameAuthorInfo.author = titleCorrector.getAuthorName(nameAuthorInfo.author);
                }
            }

            ISongImport songImport = new SongImport();
            songImport.setSongInDb(songInDb);
            songImport.setSongToImport(createSongToImport(
                    nameAuthorInfo.name, nameAuthorInfo.author, scanResult.getFiles(rootName)));

            IEvaluation eval = evaluate(songImport);
            if (nameAuthorInfo.issues != null) {
                for (IImportIssue issue : nameAuthorInfo.issues) {
                    eval.addIssue(issue);
                }
            }
            songImport.setEvaluation(eval);

            result.addSongImport(songImport);
        }

        return result;
    }

    public void doImport(IImportRecipe recipe) {
        ISongHandler songHandler = dbManager.getHandlersLookup().lookup(ISongHandler.class);
        for (ISongImport songImport : recipe.getSongImports()) {
            IEvaluation eval = songImport.getEvaluation();
            boolean isDefect = false;
            for (IImportIssue curIssue : eval.getIssues()) {
                if (IImportIssue.Severity.DEFECT.equals(curIssue.getSeverity())) {
                    isDefect = true;
                }
            }
            // don't import item with defect(s)
            if (isDefect) {
                continue;
            }

            ISong songInDb = songImport.getSongInDb();
            ISong songToImport;
            if (songInDb == null) {
                // add new song
                songToImport = songImport.getSongToImport();
            } else {
                // update existing
                songToImport = songInDb;
                ISong copyFrom = songImport.getSongToImport();
                if (eval.isImportEnabled(IEvaluation.Field.NAME)) {
                    songToImport.setName(copyFrom.getName());
                }
                if (eval.isImportEnabled(IEvaluation.Field.AUTHOR)) {
                    songToImport.setAuthor(copyFrom.getAuthor());
                }
                if (eval.isImportEnabled(IEvaluation.Field.SINGERS)) {
                    for (IBandMember singer : songToImport.getSingers()) {
                        songToImport.removeSinger(singer);
                    }
                    for (IBandMember singer : copyFrom.getSingers()) {
                        songToImport.addSinger(singer);
                    }
                }
                for (ISongResource resource : copyFrom.getResources()) {
                    if (eval.isImportEnabled(resource)) {
                        ISongResource dbResource = eval.getDbResource(resource);
                        if (dbResource != null) {
                            songToImport.removeResource(dbResource);
                        }
                        songToImport.addResource(resource);
                    }
                }
            }
            songHandler.storeSong(songToImport);
        }
        fireImportChanged();
    }

    /**
     * ISongImport.IEvaluator implementation
     */
    public IEvaluation evaluate(ISongImport songImport) {
        IEvaluation result = new SongImport.Evaluation();
        ISong songInDb = songImport.getSongInDb();
        ISong songToImport = songImport.getSongToImport();
        List<ISongResource> importRes = songToImport.getResources();

        if (importRes.isEmpty()) {
            result.addIssue(new SongImport.ImportIssue(IImportIssue.Severity.DEFECT,
                    NbBundle.getMessage(SongImporter.class, "MSG_NoResource"),
                    IImportIssue.Category.GENERAL));
        }

        if (songInDb == null) {
            List<ISongResource> textRes = findResources(importRes, 
                    IMimeTypeDetector.ContentType.TEXT_WITH_CHORDS);
            List<ISongResource> audioRes = findResources(importRes,
                    IMimeTypeDetector.ContentType.AUDIO);
            if (textRes.isEmpty() && audioRes.isEmpty()) {
                result.addIssue(new SongImport.ImportIssue(
                        IImportIssue.Severity.DEFECT,
                        NbBundle.getMessage(SongImporter.class, "MSG_NoChordsAudio"),
                        IImportIssue.Category.GENERAL));
            }
            checkAuthor(songToImport, result);
            // not now, we don't have singers implemented yet
            //checkSingers(songToImport, result);
        } else {
            evalFields(songInDb, songToImport, result);
            evalResources(songInDb.getResources(), importRes, result);
        }

        return result;
    }

    private static void checkAuthor(ISong songToImport, IEvaluation eval) {
        IAuthor author = songToImport.getAuthor();
        if (author == null || author.getName() == null ||
                author.getName().trim().isEmpty()) {
            if (!Utils.hasIssue(eval, IImportIssue.Category.AUTHOR, IImportIssue.Severity.DEFECT)) {
                eval.addIssue(new SongImport.ImportIssue(
                        IImportIssue.Severity.DEFECT,
                        NbBundle.getMessage(SongImporter.class, "MSG_NoAuthor"),
                        IImportIssue.Category.AUTHOR));
            }
        } else {
            IImportIssue toRemove = Utils.findIssue(eval,
                    IImportIssue.Category.AUTHOR, IImportIssue.Severity.DEFECT);
            if (toRemove != null) {
                eval.removeIssue(toRemove);
            }
        }
    }

    private static void checkSingers(ISong songToImport, IEvaluation result) {
        if (songToImport.getSingers() == null || songToImport.getSingers().isEmpty()) {
            result.addIssue(new SongImport.ImportIssue(
                    IImportIssue.Severity.WARNING,
                    NbBundle.getMessage(SongImporter.class, "MSG_NoSingers"),
                    IImportIssue.Category.SINGERS));
        }
    }

    private void checkSongName(ISong songToImport, IEvaluation eval) {
        String songName = songToImport.getName();
        if (songName == null || songName.isEmpty()) {
            if (!Utils.hasIssue(eval, IImportIssue.Category.NAME, IImportIssue.Severity.DEFECT)) {
                eval.addIssue(new SongImport.ImportIssue(
                        IImportIssue.Severity.DEFECT,
                        NbBundle.getMessage(SongImporter.class, "MSG_NoSongName"),
                        IImportIssue.Category.NAME));
            }
        } else {
            IImportIssue toRemove = Utils.findIssue(eval,
                    IImportIssue.Category.NAME, IImportIssue.Severity.DEFECT);
            if (toRemove != null) {
                eval.removeIssue(toRemove);
            }
        }
    }

    public void setDirectoryScanner(IDirectoryScanner dirScan) {
        this.dirScanner = dirScan;
    }

    public void setMimeTypeDetector(IMimeTypeDetector mimeDetector) {
        this.mimeDetector = mimeDetector;
    }

    public void setNameEncoder(INameEncoder encoder) {
        this.encoder = encoder;
    }

    public void setDbFactory(IBandModelFactory factory) {
        this.dbFactory = factory;
    }

    public void setDbManager(IBandManager manager) {
        this.dbManager = manager;
    }

    private ISong findSong (String songName, List<? extends ISong> songs) {
        String encodedName = encoder.encodeName(songName);
        for (ISong song : songs) {
            if (encodedName.equals(encoder.encodeName(song.getName()))) {
                return song;
            }
        }
        return null;
    }

    private ISong createSongToImport(String songName, String songAuthor, Set<File> files) {
        List<ISongResource> songResources = new ArrayList<ISongResource>();
        for (File curFile : files) {
            songResources.add(dbFactory.createSongResource(curFile.getPath(),
                    curFile.length(), curFile.lastModified(),
                    mimeDetector.getMimeType(curFile), null));
        }
        return dbFactory.createSong(songName, dbFactory.createAuthor(songAuthor),
                Collections.EMPTY_LIST, songResources);
    }

    private List<ISongResource> findResources(List<ISongResource> resources,
            IMimeTypeDetector.ContentType contentType) {
        List<ISongResource> result = new ArrayList<ISongResource>();
        for (ISongResource curRes : resources) {
            if (contentType.equals(mimeDetector.getContentType(curRes.getMimeType()))) {
                result.add(curRes);
            }
        }
        return result;
    }

    private void evalResources(List<ISongResource> dbResources, List<ISongResource> importRes,
            IEvaluation evaluation) {
        for (ISongResource dbRes : dbResources) {
            for (ISongResource impRes : importRes) {
                compareResources(dbRes, impRes, evaluation);
            }
        }
    }

    private void compareResources(ISongResource dbRes, ISongResource impRes, IEvaluation eval) {
        String dbFilePath = dbRes.getFilePath();
        String impFilePath = impRes.getFilePath();
        String dbFileName;
        String impFileName;
        String displayFileName;

        int index = dbFilePath.lastIndexOf(dbRes.getPathSeparator());
        if (index != -1) {
            displayFileName = dbFilePath.substring(index);
            dbFileName = encoder.encodeName(displayFileName);
        } else {
            displayFileName = dbFilePath;
            dbFileName = encoder.encodeName(dbFilePath);
        }
        index = impFilePath.lastIndexOf(impRes.getPathSeparator());
        if (index != -1) {
            impFileName = encoder.encodeName(impFilePath.substring(index));
        } else {
            impFileName = encoder.encodeName(impFilePath);
        }

        if (!dbFileName.equals(impFileName)) {
            // different names
            return;
        }

        /**TBD - porovnej velikost a lastModified, kdyz stejne tak info a neimportuj,
                kdyz vetsi a novejsi tak warning a importuj, jinak defect a neimportuj**/

        long dbLastModified = dbRes.getLastModified();
        long dbLength = dbRes.getLength();
        long impLastModified = impRes.getLastModified();
        long impLength = impRes.getLength();

        if (dbLastModified > 0 && dbLastModified == impLastModified &&
                dbLength > 0 && dbLength == impLength) {
            // the same resource
            eval.addIssue(new SongImport.ImportIssue(IImportIssue.Severity.INFO, dbRes, impRes,
                    NbBundle.getMessage(SongImporter.class, "MSG_ResourcesTheSame",
                    displayFileName)));
            eval.setImportEnabled(impRes, dbRes, false);
            return;
        }
        if (dbLastModified < impLastModified && dbLength < impLength) {
            eval.addIssue(new SongImport.ImportIssue(IImportIssue.Severity.WARNING, dbRes, impRes,
                    NbBundle.getMessage(SongImporter.class, "MSG_ResourceNewerBigger",
                    displayFileName)));
            eval.setImportEnabled(impRes, dbRes, true);
            return;
        }

        eval.addIssue(new SongImport.ImportIssue(IImportIssue.Severity.DEFECT, dbRes, impRes,
                NbBundle.getMessage(SongImporter.class, "MSG_ResourceDiffer",
                displayFileName)));
        eval.setImportEnabled(impRes, dbRes, false);
    }

    private void evalFields(ISong songInDb, ISong songToImport, IEvaluation result) {
        result.setImportEnabled(IEvaluation.Field.NAME, false);
        result.addIssue(new SongImport.ImportIssue(
                    IImportIssue.Severity.INFO,
                    NbBundle.getMessage(SongImporter.class, "MSG_NameInDb"),
                    IImportIssue.Category.NAME));
        if (songInDb.getAuthor() != null) {
            result.setImportEnabled(IEvaluation.Field.AUTHOR, false);
            result.addIssue(new SongImport.ImportIssue(
                        IImportIssue.Severity.INFO,
                        NbBundle.getMessage(SongImporter.class, "MSG_AuthorInDb"),
                        IImportIssue.Category.AUTHOR));
        } else {
            checkAuthor(songToImport, result);
        }
        if (songInDb.getSingers() != null && !songInDb.getSingers().isEmpty()) {
            result.setImportEnabled(IEvaluation.Field.AUTHOR, false);
            result.addIssue(new SongImport.ImportIssue(
                        IImportIssue.Severity.INFO,
                        NbBundle.getMessage(SongImporter.class, "MSG_SingersInDb"),
                        IImportIssue.Category.AUTHOR));
        } else {
            checkSingers(songToImport, result);
        }
    }

    /** package private for tests */
    NameAuthorInfo guessSongNameAndAuthor(String rootName) {
        // try author_-_song or song_-_author pattern
        NameAuthorInfo result = findAuthorSong("_-_", rootName);
        if (result.authorMatch) {
            return result;
        } else if (result.author != null) {
            result.issues = new ArrayList<IImportIssue>();
            result.issues.add(new SongImport.ImportIssue(
                    IImportIssue.Severity.WARNING,
                    NbBundle.getMessage(SongImporter.class, "MSG_AuthorNotKnown"),
                    IImportIssue.Category.AUTHOR));
        }

        // try author-song or song-author pattern
        NameAuthorInfo result2 = findAuthorSong("-", rootName);
        if (result2.authorMatch) {
            return result2;
        } else if (result2.author != null) {
            result2.issues = new ArrayList<IImportIssue>();
            result2.issues.add(new SongImport.ImportIssue(
                    IImportIssue.Severity.WARNING,
                    NbBundle.getMessage(SongImporter.class, "MSG_AuthorNotKnown"),
                    IImportIssue.Category.AUTHOR));
        }

        // rare, but try author_song or song_author pattern
        NameAuthorInfo result3 = findAuthorSong("_", rootName);
        if (result3.authorMatch) {
            return result3;
        } else if (result3.author != null) {
            result3.issues = new ArrayList<IImportIssue>();
            result3.issues.add(new SongImport.ImportIssue(
                    IImportIssue.Severity.WARNING,
                    NbBundle.getMessage(SongImporter.class, "MSG_AuthorNotKnown"),
                    IImportIssue.Category.AUTHOR));
        }

        if (result.patternMatch) {
            return result;
        }
        if (result2.patternMatch) {
            return result2;
        }
        if (result3.patternMatch) {
            return result3;
        }

        // fallback with no author
        result.author = null;
        result.name = rootName;

        return result;
    }

    
    private NameAuthorInfo findAuthorSong (String delim, String text) {
        NameAuthorInfo result = new NameAuthorInfo();
        text = text.trim();
        String[] tokens = text.split(delim);
        for (int i = 0; i < tokens.length; i++) {
            tokens[i] = tokens[i].trim();
        }
        result.patternMatch = tokens.length == 2;
        if (tokens.length > 1) {
            String token1 = tokens[0];
            String token2 = tokens[1];
            if (token1.length() < 3) {
                result.name = text.substring(text.indexOf(delim) + delim.length());
                return result;
            }
            if (token2.length() < 3) {
                result.name = text.substring(0, text.indexOf(delim));
                return result;
            }
            if (isAuthor(token1)) {
                result.author = token1;
                result.name = token2;
                result.authorMatch = true;
            } else if (isAuthor(token2)) {
                result.author = token2;
                result.name = token1;
                result.authorMatch = true;
            } else if (tokens.length == 2) {
                result.author = token1;
                result.name = token2;
                result.authorMatch = false;
            }
        } else {
            result.name = text;
        }
        return result;
    }

    private boolean isAuthor (String name) {
        // search our db
        IAuthorQuery query = dbManager.getQueriesLookup().lookup(IAuthorQuery.class);
        IAuthor author = query.findAuthor(name);
        if (author != null) {
            return true;
        }

        return existsAuthorOnNet (name);
    }

    private boolean existsAuthorOnNet(String name) {
        if (echoNest == null) {
            return false;
        }
        try {
            List<Artist> artists = echoNest.searchArtists(name, 5);
            for (Artist artist : artists) {
                if (name.equals(artist.getName())) {
                    return true;
                }
            }
        } catch (EchoNestException ex) {
            // don't report when we are offline
            if (!(ex.getCause() instanceof IOException)) {
                Exceptions.printStackTrace(ex);
            }
        }
        return false;
    }

    public void setEchoNestAPI(EchoNestAPI echoNest) {
        this.echoNest = echoNest;
    }

    public IEvaluation reevaluateAuthor(ISongImport songImport, IEvaluation evaluation) {
        checkAuthor(songImport.getSongToImport(), evaluation);
        return evaluation;
    }

    public IEvaluation reevaluateSongName(ISongImport songImport, IEvaluation evaluation) {
        checkSongName(songImport.getSongToImport(), evaluation);
        return evaluation;
    }

    public void setTitleCorrector(ITitleCorrector titleCorrector) {
        this.titleCorrector = titleCorrector;
    }

    public void addImportObserver(final IImportObserver io) {
        ioList.add(io);
    }

    public void removeImportObserver(IImportObserver io) {
        ioList.remove(io);
    }

    private void fireImportChanged() {
        for (IImportObserver io : ioList) {
            io.importPerformed();
        }
    }

    /** package private for tests */
    static class NameAuthorInfo {
        String name;
        String author;
        boolean authorMatch;
        boolean patternMatch;
        List<IImportIssue> issues;
    }

}
